/*
 * Copyright (c) 2018, 2024 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.security.providers.oidc.common;

import java.lang.System.Logger.Level;
import java.net.URI;
import java.time.Duration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.function.Supplier;

import io.helidon.common.Errors;
import io.helidon.common.LazyValue;
import io.helidon.common.config.Config;
import io.helidon.common.configurable.Resource;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;
import io.helidon.cors.CrossOriginConfig;
import io.helidon.http.SetCookie;
import io.helidon.security.Security;
import io.helidon.security.SecurityException;
import io.helidon.security.jwt.jwk.JwkKeys;
import io.helidon.security.providers.oidc.common.spi.TenantConfigFinder;
import io.helidon.security.util.TokenHandler;
import io.helidon.webclient.api.WebClient;
import io.helidon.webclient.api.WebClientConfig;

/**
 * Configuration of OIDC usable from all resources that utilize OIDC specification, such as security provider, web server
 * extension and IDCS connectivity.
 * <p>
 * Some of the configuration options below use "resource" type. The following configuration
 * can be used for a resource (example for oidc-metadata key):
 * {@code
 * oidc-metadata-path: "path/on/filesystem"
 * oidc-metadata-resource-path: "class-path/resource"
 * oidc-metadata-url: "URI on the net"
 * oidc-metadata-content-plain: "Value of the resource in plain text"
 * oidc-metadata-content: "Value in base64 encoded bytes"
 * }
 * <p>
 * Configuration options required (under security.providers[].${name}):
 * <table class="config">
 * <caption>Mandatory configuration parameters</caption>
 * <tr>
 *     <th>key</th>
 *     <th>description</th>
 * </tr>
 * <tr>
 *     <td>client-id</td>
 *     <td>Client ID as generated by OIDC server</td>
 * </tr>
 * <tr>
 *     <td>client-secret</td>
 *     <td>Client secret as generated by OIDC server</td>
 * </tr>
 * <tr>
 *     <td>identity-uri</td>
 *     <td>URI of the identity server, base used to retrieve OIDC metadata</td>
 * </tr>
 * <tr>
 *     <td>frontend-uri</td>
 *     <td>Full URI of the frontend for redirects back from OIDC server (e.g. http://myserver/myApp)</td>
 * </tr>
 * </table>
 *
 * <table class="config">
 * <caption>Optional configuration parameters</caption>
 * <tr>
 *     <th>key</th>
 *     <th>default value</th>
 *     <th>description</th>
 * </tr>
 * <tr>
 *     <td>proxy-protocol</td>
 *     <td>http</td>
 *     <td>Proxy protocol to use when proxy is used.</td>
 * </tr>
 * <tr>
 *     <td>proxy-host</td>
 *     <td>null</td>
 *     <td>Proxy host to use. When defined, triggers usage of proxy for HTTP requests.</td>
 * </tr>
 * <tr>
 *     <td>proxy-port</td>
 *     <td>80</td>
 *     <td>Port of the proxy server to use</td>
 * </tr>
 * <tr>
 *     <td>relative-uris</td>
 *     <td>false</td>
 *     <td>Flag to force the use of relative URIs in all requests. By default,
 *          requests that use the Proxy will have absolute URIs. Set this flag to
 *          true if the host is unable to accept absolute URIs.</td>
 * </tr>
 * <tr>
 *     <td>redirect-uri</td>
 *     <td>/oidc/redirect</td>
 *     <td>URI to register web server component on, used by the OIDC server to
 *          redirect authorization requests to after a user logs in or approves scopes. Note that usually the redirect URI
 *          configured here must be the same one as configured on OIDC server.</td>
 * </tr>
 * <tr>
 *     <td>scope-audience</td>
 *     <td>empty string</td>
 *     <td>Audience of the scope required by this application. This is prefixed to
 *          the scope name when requesting scopes from the identity server.</td>
 * </tr>
 * <tr>
 *     <td>cookie-use</td>
 *     <td>true</td>
 *     <td>Whether to use cookie to store JWT. If used, redirects happen only in case the user
 *              is not authenticated or has insufficient scopes</td>
 * </tr>
 * <tr>
 *     <td>cookie-name</td>
 *     <td>JSESSIONID</td>
 *     <td>Name of the cookie</td>
 * </tr>
 * <tr>
 *     <td>cookie-domain</td>
 *     <td>null</td>
 *     <td>Domain the cookie is valid for. Not used by default</td>
 * </tr>
 * <tr>
 *     <td>cookie-path</td>
 *     <td>/</td>
 *     <td>Path the cookie is valid for.</td>
 * </tr>
 * <tr>
 *     <td>cookie-max-age-seconds</td>
 *     <td>null</td>
 *     <td>When using cookie, used to set MaxAge attribute of the cookie, defining
 *              how long the cookie is valid.</td>
 * </tr>
 * <tr>
 *     <td>cookie-http-only</td>
 *     <td>true</td>
 *     <td>When using cookie, if set to true, the HttpOnly attribute will be configured.</td>
 * </tr>
 * <tr>
 *     <td>cookie-secure</td>
 *     <td>false</td>
 *     <td>When using cookie, if set to true, the Secure attribute will be configured.</td>
 * </tr>
 * <tr>
 *     <td>cookie-same-site</td>
 *     <td>Lax</td>
 *     <td>When using cookie, used to set the SameSite cookie value. Can be "Strict" or "Lax".
 *     Setting this to "Strict" will result in infinite redirects when calling OIDC on a different host.
 *     </td>
 * </tr>
 * <tr>
 *     <td>query-param-use</td>
 *     <td>false</td>
 *     <td>Whether to expect JWT in a query parameter</td>
 * </tr>
 * <tr>
 *     <td>query-param-name</td>
 *     <td>accessToken</td>
 *     <td>Name of a query parameter that contains the JWT token when parameter is used.</td>
 * </tr>
 * <tr>
 *     <td>header-use</td>
 *     <td>true</td>
 *     <td>Whether to expect JWT in a header field.</td>
 * </tr>
 * <tr>
 *     <td>header-token</td>
 *     <td>"Authorization" header with prefix "bearer "</td>
 *     <td>A {@link TokenHandler} configuration to process header containing a JWT</td>
 * </tr>
 * <tr>
 *     <td>oidc-metadata-well-known</td>
 *     <td>true</td>
 *     <td>If set to true, metadata will be loaded from default (well known)
 *          location, unless it is explicitly defined using oidc-metadata-resource. If set to false, it would not be loaded even
 *          if oidc-metadata-resource is not defined. In such a case all URIs must be explicitly
 *          defined (e.g. token-endpoint-uri).</td>
 * </tr>
 * <tr>
 *     <td>oidc-metadata.resource</td>
 *     <td>identity-uri/.well-known/openid-configuration</td>
 *     <td>Resource configuration for OIDC Metadata containing endpoints to various identity services, as well as information
 *     about the identity server. See {@link Resource#create(io.helidon.common.config.Config)}</td>
 * </tr>
 * <tr>
 *     <td>token-endpoint-uri</td>
 *     <td>token_endpoint in OIDC metadata, or identity-url/oauth2/v1/token if not available</td>
 *     <td>URI of a token endpoint used to obtain a JWT based on the authentication code.</td>
 * </tr>
 * <tr>
 *     <td>authorization-endpoint-uri</td>
 *     <td>"authorization_endpoint" in OIDC metadata, or identity-uri/oauth2/v1/authorize if not available</td>
 *     <td>URI of an authorization endpoint used to redirect users to for logging-in.</td>
 * </tr>
 * <tr>
 *     <td>validate-jwt-with-jwk</td>
 *     <td>true</td>
 *     <td>When true  - validate against jwk defined by "sign-jwk", when false
 *          validate JWT through OIDC Server endpoint "validation-endpoint-uri"</td>
 * </tr>
 * <tr>
 *     <td>sign-jwk.resource</td>
 *     <td>"jwks-uri" in OIDC metadata, or identity-uri/admin/v1/SigningCert/jwk if not available, only needed
 *              when jwt validation is done by us</td>
 *     <td>A resource pointing to JWK with public keys of signing certificates used to validate JWT.
 *     See {@link Resource#create(io.helidon.common.config.Config)}</td>
 * </tr>
 * <tr>
 *     <td>introspect-endpoint-uri</td>
 *     <td>"introspection_endpoint" in OIDC metadata, or identity-uri/oauth2/v1/introspect</td>
 *     <td>When validate-jwt-with-jwk is set to "false", this is the endpoint used</td>
 * </tr>
 * <tr>
 *     <td>base-scopes</td>
 *     <td>{@value Builder#DEFAULT_BASE_SCOPES}</td>
 *     <td>Configure scopes to be requested by default. If the scope has a qualifier, it must be included here</td>
 * </tr>
 * <tr>
 *     <td>redirect</td>
 *     <td>false</td>
 *     <td>Whether to redirect to identity server when authentication failed.</td>
 * </tr>
 * <tr>
 *     <td>realm</td>
 *     <td>helidon</td>
 *     <td>Realm returned in HTTP response if redirect is not enabled or possible.</td>
 * </tr>
 * <tr>
 *     <td>redirect-attempt-param</td>
 *     <td>{@value DEFAULT_ATTEMPT_PARAM}</td>
 *     <td>Query parameter holding the number of times we redirected to an identity server. Customizable to prevent
 *     conflicts with application parameters</td>
 * </tr>
 * <tr>
 *     <td>max-redirects</td>
 *     <td>{@value DEFAULT_MAX_REDIRECTS}</td>
 *     <td>Maximal number of times we can redirect to an identity server. When the number is reached, no further redirects
 *     happen and the request finishes with an error (status {@code 401})</td>
 * </tr>
 * <tr>
 *     <td>server-type</td>
 *     <td>&nbsp;</td>
 *     <td>Type of identity server. Currently supported is {@code idcs} or not configured (for default).</td>
 * </tr>
 * <tr>
 *     <td>{@code client-timeout-millis}</td>
 *     <td>30 seconds</td>
 *     <td>Timeout on HTTP client calls</td>
 * </tr>
 * <tr>
 *     <td>{@code cookie-encryption-enabled}</td>
 *     <td>Depends on other configuration</td>
 *     <td>Whether cookies should be encrypted. Will be enabled if logout is enabled.</td>
 * </tr>
 * <tr>
 *     <td>{@code cookie-encryption-password}</td>
 *     <td>Generated for this service (as a file)</td>
 *     <td>Encryption password to be used for symmetric cipher. Must be the same for all services that are intended
 *     to share a cookie as a form of authentication</td>
 * </tr>
 * <tr>
 *     <td>{@code cookie-encryption-name}</td>
 *     <td>&nbsp;</td>
 *     <td>Name of encryption configuration in {@link io.helidon.security.Security}. If used, security must be registered
 *     in curent context or in global context (this is done automatically in Helidon MP).</td>
 * </tr>
 * <tr>
 *     <td>{@code logout-endpoint-uri}</td>
 *     <td>From well known metadata endpoint</td>
 *     <td>Endpoint to redirect user to log out from OIDC server.</td>
 * </tr>
 * <tr>
 *     <td>{@code post-logout-uri}</td>
 *     <td>&nbsp;</td>
 *     <td>Required if logout is enabled. Endpoint the OIDC server redirects back to after logging user out.</td>
 * </tr>
 * <tr>
 *     <td>{@code logout-enabled}</td>
 *     <td>{@code false}</td>
 *     <td>Whether logout support should be enabled. Requires encryption of cookies (and cookies must be used).</td>
 * </tr>
 * <tr>
 *     <td>{@code cors}</td>
 *     <td>&nbsp;</td>
 *     <td>Cross-origin resource sharing settings. See {@link io.helidon.cors.CrossOriginConfig}.</td>
 * </tr>
 * <tr>
 *     <td>{@code force-https-redirects}</td>
 *     <td>&nbsp;</td>
 *     <td>Force https for redirects to identity provider.
 *     This is helpful if you have a frontend SSL or cloud load balancer in front and Helidon is serving plain http.</td>
 * </tr>
 * <tr>
 *     <td>{@code optional-audience}</td>
 *     <td>{@code false}</td>
 *     <td>Allow audience claim to be optional.</td>
 * </tr>
 * <tr>
 *     <td>{@code check-audience}</td>
 *     <td>{@code true}</td>
 *     <td>Turn audience claim check on when {@code true} or off when {@code false}.</td>
 * </tr>
 * </table>
 */
public final class OidcConfig extends TenantConfigImpl {
    /**
     * Default name of the header we expect JWT in.
     */
    public static final String PARAM_HEADER_NAME = "X_OIDC_TOKEN_HEADER";
    /**
     * Default name of the header we expect JWT in.
     */
    public static final String PARAM_ID_HEADER_NAME = "X_OIDC_ID_TOKEN_HEADER";
    /**
     * Default tenant query param name.
     */
    public static final String DEFAULT_TENANT_PARAM_NAME = "h_tenant";
    /**
     * Default access token cookie name.
     */
    public static final String DEFAULT_COOKIE_NAME = "JSESSIONID";
    /**
     * Default id token cookie name.
     */
    public static final String DEFAULT_ID_COOKIE_NAME = DEFAULT_COOKIE_NAME + "_2";
    /**
     * Default refresh token cookie name.
     */
    public static final String DEFAULT_REFRESH_COOKIE_NAME = DEFAULT_COOKIE_NAME + "_3";
    /**
     * Default tenant cookie name.
     */
    public static final String DEFAULT_TENANT_COOKIE_NAME = "HELIDON_TENANT";
    /**
     * Default state cookie name.
     */
    public static final String DEFAULT_STATE_COOKIE_NAME = "OIDC_STATE";
    static final String DEFAULT_REDIRECT_URI = "/oidc/redirect";
    static final String DEFAULT_LOGOUT_URI = "/oidc/logout";
    static final boolean DEFAULT_REDIRECT = true;
    static final String DEFAULT_ATTEMPT_PARAM = "h_ra";
    static final int DEFAULT_MAX_REDIRECTS = 5;
    static final boolean DEFAULT_FORCE_HTTPS_REDIRECTS = false;
    static final Duration DEFAULT_TOKEN_REFRESH_SKEW = Duration.ofSeconds(5);
    static final boolean DEFAULT_RELATIVE_URIS = false;
    static final int DEFAULT_PROXY_PORT = 80;
    static final String DEFAULT_PROXY_PROTOCOL = "http";
    static final String TENANT_IDENT = "name";
    static final String DEFAULT_PARAM_NAME = "accessToken";
    static final String DEFAULT_ID_TOKEN_PARAM_NAME = "id_token";
    static final boolean DEFAULT_PARAM_USE = false;
    static final boolean DEFAULT_HEADER_USE = false;
    static final boolean DEFAULT_COOKIE_USE = true;

    private static final System.Logger LOGGER = System.getLogger(OidcConfig.class.getName());

    private final Map<String, TenantConfig> tenantConfigurations;
    private final String redirectUri;
    private final String logoutUri;
    private final boolean logoutEnabled;
    private final String frontendUri;
    private final boolean redirect;
    private final String redirectAttemptParam;
    private final int maxRedirects;
    private final URI postLogoutUri;
    private final CrossOriginConfig crossOriginConfig;
    private final boolean forceHttpsRedirects;
    private final Duration tokenRefreshSkew;
    private final boolean relativeUris;
    private final WebClient webClient;
    private final Supplier<WebClientConfig.Builder> webClientBuilderSupplier;
    private final LazyValue<Tenant> defaultTenant;
    private final boolean useParam;
    private final String paramName;
    private final String idTokenParamName;
    private final String tenantParamName;
    private final boolean useHeader;
    private final TokenHandler headerHandler;
    private final boolean useCookie;
    private final OidcCookieHandler tokenCookieHandler;
    private final OidcCookieHandler idTokenCookieHandler;
    private final OidcCookieHandler refreshTokenCookieHandler;
    private final OidcCookieHandler tenantCookieHandler;
    private final OidcCookieHandler stateCookieHandler;
    private final boolean tokenSignatureValidation;
    private final boolean idTokenSignatureValidation;
    private final boolean accessTokenIpCheck;

    private OidcConfig(Builder builder) {
        super(builder);
        this.frontendUri = builder.frontendUri;
        this.redirectUri = builder.redirectUri;
        this.logoutUri = builder.logoutUri;
        this.logoutEnabled = builder.logoutEnabled;
        this.postLogoutUri = builder.postLogoutUri;
        this.redirect = builder.redirect;
        this.redirectAttemptParam = builder.redirectAttemptParam;
        this.maxRedirects = builder.maxRedirects;
        this.forceHttpsRedirects = builder.forceHttpsRedirects;
        this.crossOriginConfig = builder.crossOriginConfig;
        this.tokenRefreshSkew = builder.tokenRefreshSkew;
        this.tenantConfigurations = Map.copyOf(builder.tenantConfigurations);
        this.webClient = builder.webClient;
        this.relativeUris = builder.relativeUris;

        this.useParam = builder.useParam;
        this.paramName = builder.paramName;
        this.idTokenParamName = builder.idTokenParamName;
        this.tenantParamName = builder.tenantParamName;
        this.useHeader = builder.useHeader;
        this.headerHandler = builder.headerHandler;
        this.useCookie = builder.useCookie;
        this.tokenCookieHandler = builder.tokenCookieBuilder.build();
        this.idTokenCookieHandler = builder.idTokenCookieBuilder.build();
        this.tenantCookieHandler = builder.tenantCookieBuilder.build();
        this.refreshTokenCookieHandler = builder.refreshTokenCookieBuilder.build();
        this.stateCookieHandler = builder.stateCookieBuilder.build();
        this.tokenSignatureValidation = builder.tokenSignatureValidation;
        this.idTokenSignatureValidation = builder.idTokenSignatureValidation;
        this.accessTokenIpCheck = builder.accessTokenIpCheck;

        this.webClientBuilderSupplier = builder.webClientBuilderSupplier;
        this.defaultTenant = LazyValue.create(() -> Tenant.create(this, this));

        LOGGER.log(Level.TRACE, () -> "Redirect URI with host: " + frontendUri + redirectUri);
    }

    /**
     * Create a builder to programmatically construct OIDC configuration.
     *
     * @return a new builder instance usable for fluent API
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Create a new instance from {@link io.helidon.common.config.Config}.
     * The config instance has to be on the node containing keys used by this class (e.g. client-id).
     *
     * @param config configuration used to obtain OIDC integration values
     * @return a new instance of this class configured from provided config
     */
    public static OidcConfig create(Config config) {
        return OidcConfig.builder()
                .config(config)
                .build();
    }

    /**
     * Whether to use query parameter to get the information from request.
     *
     * @return if query parameter should be used
     * @see Builder#useParam(Boolean)
     */
    public boolean useParam() {
        return useParam;
    }

    /**
     * Query parameter name.
     *
     * @return name of the query parameter to use
     * @see Builder#paramName(String)
     */
    public String paramName() {
        return paramName;
    }

    /**
     * Query id token parameter name.
     *
     * @return name of the query parameter to use
     * @see Builder#idTokenParamName(String)
     */
    public String idTokenParamName() {
        return idTokenParamName;
    }

    /**
     * Tenant query parameter name.
     *
     * @return name of the tenant query parameter to use
     * @see Builder#paramTenantName(String)
     */
    public String tenantParamName() {
        return tenantParamName;
    }

    /**
     * Whether to use HTTP header to get the information from request.
     *
     * @return if header should be used
     * @see Builder#useHeader(Boolean)
     */
    public boolean useHeader() {
        return useHeader;
    }

    /**
     * {@link TokenHandler} to extract header information from request.
     *
     * @return handler to extract header
     * @see Builder#headerTokenHandler(TokenHandler)
     */
    public TokenHandler headerHandler() {
        return headerHandler;
    }

    /**
     * Whether to use cooke to get the information from request.
     *
     * @return if cookie should be used
     * @see Builder#useCookie(Boolean)
     */
    public boolean useCookie() {
        return useCookie;
    }

    /**
     * Cookie handler to create cookies or unset cookies for token.
     *
     * @return a new cookie handler
     */
    public OidcCookieHandler tokenCookieHandler() {
        return tokenCookieHandler;
    }

    /**
     * Cookie handler to create cookies or unset cookies for id token.
     *
     * @return a new cookie handler
     */
    public OidcCookieHandler idTokenCookieHandler() {
        return idTokenCookieHandler;
    }

    /**
     * Cookie handler to create cookies or unset cookies for tenant name.
     *
     * @return a new cookie handler
     */
    public OidcCookieHandler tenantCookieHandler() {
        return tenantCookieHandler;
    }

    /**
     * Cookie handler to create cookies or unset cookies for refresh token.
     *
     * @return a new cookie handler
     */
    public OidcCookieHandler refreshTokenCookieHandler() {
        return refreshTokenCookieHandler;
    }

    /**
     * Cookie handler to create cookies or unset cookies for state value.
     *
     * @return a new cookie handler
     */
    public OidcCookieHandler stateCookieHandler() {
        return stateCookieHandler;
    }

    /**
     * Redirection URI.
     *
     * @return uri the OIDC server redirects back to
     * @see Builder#redirectUri(String)
     */
    public String redirectUri() {
        return redirectUri;
    }

    /**
     * Whether to force https when redirecting to identity provider.
     *
     * @return {@code true} to force use of https
     */
    public boolean forceHttpsRedirects() {
        return forceHttpsRedirects;
    }

    /**
     * Whether logout is enabled.
     *
     * @return {@code true} if logout is enabled
     */
    public boolean logoutEnabled() {
        return logoutEnabled;
    }

    /**
     * Logout URI.
     *
     * @return uri that processes logout in Helidon and redirects to OIDC server logout
     * @see Builder#logoutUri(String)
     */
    public String logoutUri() {
        return logoutUri;
    }

    /**
     * Post logout redirect URI.
     *
     * @return uri that OIDC server redirects to once logout is finished
     * @see Builder#postLogoutUri(java.net.URI)
     */
    public URI postLogoutUri() {
        return postLogoutUri;
    }

    /**
     * Redirect URI with host information.
     *
     * @return redirect URI
     * @see Builder#redirectUri(String)
     */
    public String redirectUriWithHost() {
        if (frontendUri == null) {
            throw new SecurityException("Frontend URI is not defined");
        }
        return frontendUri + redirectUri;
    }

    /**
     * Redirect URI with host information taken from request,
     * unless an explicit frontend uri is defined in configuration.
     *
     * @param frontendUri the frontend uri
     * @return redirect URI
     */
    public String redirectUriWithHost(String frontendUri) {
        if (this.frontendUri != null) {
            return redirectUriWithHost();
        }
        return frontendUri + this.redirectUri;
    }

    /**
     * Whether to redirect to identity server if user is not authenticated.
     *
     * @return whether to redirect, defaults to true
     */
    public boolean shouldRedirect() {
        return redirect;
    }

    /**
     * Name of the parameter used in state passed to OIDC to store the number of attempted redirects.
     * This is to prevent infinite redirects.
     *
     * @return name of the query parameter
     */
    public String redirectAttemptParam() {
        return redirectAttemptParam;
    }

    /**
     * Maximal number of redirects allowed between Helidon and OIDC provider.
     *
     * @return maximal number of redirects
     */
    public int maxRedirects() {
        return maxRedirects;
    }

    /**
     * Cross-origin resource sharing settings.
     *
     * @return CORS settings
     */
    public CrossOriginConfig crossOriginConfig() {
        return crossOriginConfig;
    }

    /**
     * Amount of time access token should be refreshed before its expiration time.
     *
     * @return refresh time skew
     */
    public Duration tokenRefreshSkew() {
        return tokenRefreshSkew;
    }

    /**
     * Determines whether to force the use of relative URIs in all requests,
     * regardless of the presence or absence of proxies or no-proxy lists.
     *
     * @return {@code true} if we should use relative URIs
     */
    public boolean relativeUris() {
        return relativeUris;
    }

    /**
     * Client with configured proxy with no security.
     *
     * @return client for general use.
     */
    public WebClient generalWebClient() {
        return webClient;
    }

    /**
     * Client with configured proxy and security.
     *
     * @return client for communicating with OIDC identity server
     */
    public WebClient appWebClient() {
        return defaultTenant.get().appWebClient();
    }

    /**
     * Return {@link TenantConfig} bound to the provided tenant id.
     * If no {@link TenantConfig} found, default OIDC configuration should be returned.
     *
     * @param tenantId tenant id of the configuration
     * @return configuration bound to the tenant id, or default oidc configuration if not found
     */
    public TenantConfig tenantConfig(String tenantId) {
        TenantConfig tenantConfig = tenantConfigurations.get(tenantId);
        if (tenantConfig == null) {
            return tenantConfigurations.getOrDefault(TenantConfigFinder.DEFAULT_TENANT_ID, this);
        }
        return tenantConfig;
    }

    /**
     * Token endpoint URI.
     *
     * @return endpoint URI
     */
    public URI tokenEndpointUri() {
        return defaultTenant.get().tokenEndpointUri();
    }

    /**
     * Authorization endpoint.
     *
     * @return authorization endpoint uri as a string
     */
    public String authorizationEndpointUri() {
        return defaultTenant.get().authorizationEndpointUri();
    }

    /**
     * Logout endpoint on OIDC server.
     *
     * @return URI of the logout endpoint
     * @see OidcConfig.Builder#logoutEndpointUri(java.net.URI)
     */
    public URI logoutEndpointUri() {
        return defaultTenant.get().logoutEndpointUri();
    }

    /**
     * Token issuer.
     *
     * @return token issuer
     * @see OidcConfig.Builder#issuer(String)
     */
    public String issuer() {
        return defaultTenant.get().issuer();
    }

    /**
     * JWK used for signature validation.
     *
     * @return set of keys used use to verify tokens
     */
    public JwkKeys signJwk() {
        return defaultTenant.get().signJwk();
    }

    /**
     * Introspection endpoint URI.
     *
     * @return introspection endpoint URI
     * @see OidcConfig.Builder#introspectEndpointUri(java.net.URI)
     */
    public URI introspectUri() {
        return defaultTenant.get().introspectUri();
    }

    /**
     * Whether access token signature should be validated.
     *
     * @return validate access token signature
     */
    public boolean tokenSignatureValidation() {
        return tokenSignatureValidation;
    }

    /**
     * Whether id token signature should be validated.
     *
     * @return validate id token signature
     */
    public boolean idTokenSignatureValidation() {
        return idTokenSignatureValidation;
    }

    /**
     * Whether to check IP address access token was issued for.
     *
     * @return whether to check IP address access token was issued for
     */
    public boolean accessTokenIpCheck() {
        return accessTokenIpCheck;
    }

    Supplier<WebClientConfig.Builder> webClientBuilderSupplier() {
        return webClientBuilderSupplier;
    }

    /**
     * Client Authentication methods that are used by Clients to authenticate to the Authorization
     * Server when using the Token Endpoint.
     */
    public enum ClientAuthentication {
        /**
         * Clients that have received a client_secret value from the Authorization Server authenticate with the Authorization
         * Server in accordance with Section 2.3.1 of OAuth 2.0 [RFC6749] using the HTTP Basic authentication scheme.
         * This is the default client authentication.
         */
        CLIENT_SECRET_BASIC,
        /**
         * Clients that have received a client_secret value from the Authorization Server, authenticate with the Authorization
         * Server in accordance with Section 2.3.1 of OAuth 2.0 [RFC6749] by including the Client Credentials in the request body.
         */
        CLIENT_SECRET_POST,
        /**
         * Clients that have received a client_secret value from the Authorization Server create a JWT using an HMAC SHA
         * algorithm, such as HMAC SHA-256. The HMAC (Hash-based Message Authentication Code) is calculated using the octets of
         * the UTF-8 representation of the client_secret as the shared key.
         * The Client authenticates in accordance with JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
         * Authorization Grants [OAuth.JWT] and Assertion Framework for OAuth 2.0 Client Authentication and Authorization
         * Grants [OAuth.Assertions].
         * <p>
         * The JWT MUST contain the following REQUIRED Claim Values and MAY contain the following
         * OPTIONAL Claim Values.
         * <p>
         * Required:
         * {@code iss, sub, aud, jti, exp}
         * <p>
         * Optional:
         * {@code iat}
         */
        CLIENT_SECRET_JWT,
        /**
         * Clients that have registered a public key sign a JWT using that key. The Client authenticates in accordance with
         * JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.JWT] and Assertion
         * Framework for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.Assertions].
         * <p>
         * The JWT MUST contain the following REQUIRED Claim Values and MAY contain the following
         * OPTIONAL Claim Values.
         * <p>
         * Required:
         * {@code iss, sub, aud, jti, exp}
         * <p>
         * Optional:
         * {@code iat}
         */
        PRIVATE_KEY_JWT,
        /**
         * The Client does not authenticate itself at the Token Endpoint, either because it uses only the Implicit Flow (and so
         * does not use the Token Endpoint) or because it is a Public Client with no Client Secret or other authentication
         * mechanism.
         */
        NONE
    }

    /**
     * Types of requests to identity provider.
     */
    public enum RequestType {
        /**
         * Request to exchange code for a token issued against the token endpoint.
         */
        CODE_TO_TOKEN,
        /**
         * Request to validate a JWT against an introspection endpoint.
         */
        INTROSPECT_JWT;
    }

    /**
     * A fluent API {@link io.helidon.common.Builder} to build instances of {@link OidcConfig}.
     */
    @Configured(description = "Open ID Connect configuration")
    public static class Builder extends BaseBuilder<Builder, OidcConfig> {

        private final Map<String, TenantConfig> tenantConfigurations = new HashMap<>();

        // mandatory properties
        private String redirectUri = DEFAULT_REDIRECT_URI;
        private String logoutUri = DEFAULT_LOGOUT_URI;
        private boolean logoutEnabled = false;
        // optional properties
        private String frontendUri;
        private boolean redirect = DEFAULT_REDIRECT;
        private String redirectAttemptParam = DEFAULT_ATTEMPT_PARAM;
        private int maxRedirects = DEFAULT_MAX_REDIRECTS;
        private URI postLogoutUri;
        private CrossOriginConfig crossOriginConfig;
        private boolean forceHttpsRedirects = DEFAULT_FORCE_HTTPS_REDIRECTS;
        private Duration tokenRefreshSkew = DEFAULT_TOKEN_REFRESH_SKEW;
        private String proxyHost;
        private String proxyProtocol = DEFAULT_PROXY_PROTOCOL;
        private int proxyPort = DEFAULT_PROXY_PORT;
        private WebClient webClient;
        private Supplier<WebClientConfig.Builder> webClientBuilderSupplier;
        private String paramName = DEFAULT_PARAM_NAME;
        private String idTokenParamName = DEFAULT_ID_TOKEN_PARAM_NAME;
        private String tenantParamName = DEFAULT_TENANT_PARAM_NAME;
        private boolean useHeader = DEFAULT_HEADER_USE;
        private boolean useParam = DEFAULT_PARAM_USE;

        private final OidcCookieHandler.Builder tenantCookieBuilder = OidcCookieHandler.builder()
                .encryptionEnabled(true)
                .cookieName(DEFAULT_TENANT_COOKIE_NAME);
        private final OidcCookieHandler.Builder tokenCookieBuilder = OidcCookieHandler.builder()
                .cookieName(DEFAULT_COOKIE_NAME);
        private final OidcCookieHandler.Builder idTokenCookieBuilder = OidcCookieHandler.builder()
                .encryptionEnabled(true)
                .cookieName(DEFAULT_ID_COOKIE_NAME);
        private final OidcCookieHandler.Builder refreshTokenCookieBuilder = OidcCookieHandler.builder()
                .encryptionEnabled(true)
                .cookieName(DEFAULT_REFRESH_COOKIE_NAME);
        private final OidcCookieHandler.Builder stateCookieBuilder = OidcCookieHandler.builder()
                .encryptionEnabled(true)
                .cookieName(DEFAULT_STATE_COOKIE_NAME);
        private TokenHandler headerHandler = TokenHandler.builder()
                .tokenHeader("Authorization")
                .tokenPrefix("bearer ")
                .build();
        private boolean useCookie = DEFAULT_COOKIE_USE;
        private boolean cookieSameSiteDefault = true;
        private boolean relativeUris = DEFAULT_RELATIVE_URIS;
        private boolean tokenSignatureValidation = true;
        private boolean idTokenSignatureValidation = true;
        private boolean accessTokenIpCheck = true;

        protected Builder() {
        }

        @Override
        public OidcConfig build() {
            buildConfiguration();

            Errors.Collector collector = Errors.collector();
            if (useCookie && logoutEnabled) {
                if (postLogoutUri == null) {
                    collector.fatal("post-logout-uri must be defined when logout is enabled.");
                }
            }

            // second set of validations
            collector.collect().checkValid();

            if (cookieSameSiteDefault && useCookie) {
                // compare frontend and oidc endpoints to see if
                // we should use lax or strict by default
                if (identityUri() != null) {
                    String identityHost = identityUri().getHost();
                    if (frontendUri != null) {
                        String frontendHost = URI.create(frontendUri).getHost();
                        if (identityHost.equals(frontendHost)) {
                            LOGGER.log(Level.INFO, "As frontend host and identity host are equal, setting Same-Site policy"
                                    + " to Strict this can be overridden using configuration option of OIDC: "
                                    + "\"cookie-same-site\"");
                            this.tenantCookieBuilder.sameSite(SetCookie.SameSite.STRICT);
                            this.tokenCookieBuilder.sameSite(SetCookie.SameSite.STRICT);
                            this.idTokenCookieBuilder.sameSite(SetCookie.SameSite.STRICT);
                        }
                    }
                }
            }

            this.webClientBuilderSupplier = () -> OidcUtil.webClientBaseBuilder(proxyProtocol,
                                                                                proxyHost,
                                                                                proxyPort,
                                                                                relativeUris,
                                                                                clientTimeout());
            this.webClient = webClientBuilderSupplier.get().build();

            if (!tokenSignatureValidation) {
                LOGGER.log(Level.WARNING, "You have disabled access token signature validation. "
                        + "This option should never be disabled for production environment "
                        + "since it could cause security issues");
            }
            if (!idTokenSignatureValidation) {
                LOGGER.log(Level.WARNING, "You have disabled id token signature validation. "
                        + "This option should never be disabled for production environment "
                        + "since it could cause security issues");
            }

            return new OidcConfig(this);
        }

        /**
         * Update this builder with values from configuration.
         *
         * @param config configuration located on node with OIDC configuration keys (e.g. client-id)
         * @return updated builder instance
         */
        public Builder config(Config config) {
            super.config(config);
            // mandatory configuration
            config.get("frontend-uri").asString().ifPresent(this::frontendUri);

            // environment
            config.get("proxy-protocol")
                    .asString()
                    .ifPresent(this::proxyProtocol);
            config.get("proxy-host").asString().ifPresent(this::proxyHost);
            config.get("proxy-port").asInt().ifPresent(this::proxyPort);
            config.get("relative-uris").asBoolean().ifPresent(this::relativeUris);

            // token handling
            config.get("query-param-use").asBoolean().ifPresent(this::useParam);
            config.get("query-param-name").asString().ifPresent(this::paramName);
            config.get("query-param-tenant-name").asString().ifPresent(this::paramTenantName);
            config.get("header-use").asBoolean().ifPresent(this::useHeader);
            config.get("header-token").as(TokenHandler.class).ifPresent(this::headerTokenHandler);
            config.get("cookie-use").asBoolean().ifPresent(this::useCookie);
            config.get("cookie-name").asString().ifPresent(this::cookieName);
            config.get("cookie-name-id-token").asString().ifPresent(this::cookieNameIdToken);
            config.get("cookie-name-tenant").asString().ifPresent(this::cookieTenantName);
            config.get("cookie-name-refresh-token").asString().ifPresent(this::cookieNameRefreshToken);
            config.get("cookie-name-state").asString().ifPresent(this::cookieNameState);
            config.get("cookie-domain").asString().ifPresent(this::cookieDomain);
            config.get("cookie-path").asString().ifPresent(this::cookiePath);
            config.get("cookie-max-age-seconds").asLong().ifPresent(this::cookieMaxAgeSeconds);
            config.get("cookie-http-only").asBoolean().ifPresent(this::cookieHttpOnly);
            config.get("cookie-secure").asBoolean().ifPresent(this::cookieSecure);
            config.get("cookie-same-site").asString().ifPresent(this::cookieSameSite);
            // encryption of cookies
            config.get("cookie-encryption-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabled);
            config.get("cookie-encryption-id-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledIdToken);
            config.get("cookie-encryption-tenant-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledTenantName);
            config.get("cookie-encryption-refresh-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledRefreshToken);
            config.get("cookie-encryption-state-enabled").asBoolean().ifPresent(this::cookieEncryptionEnabledState);
            config.get("cookie-encryption-password").as(String.class)
                    .map(String::toCharArray)
                    .ifPresent(this::cookieEncryptionPassword);
            config.get("cookie-encryption-name").asString().ifPresent(this::cookieEncryptionName);

            // our application
            config.get("redirect-uri").asString().ifPresent(this::redirectUri);
            config.get("logout-uri").asString().ifPresent(this::logoutUri);

            config.get("post-logout-uri").as(URI.class).ifPresent(this::postLogoutUri);
            config.get("logout-enabled").asBoolean().ifPresent(this::logoutEnabled);

            config.get("redirect").asBoolean().ifPresent(this::redirect);
            config.get("redirect-attempt-param").asString().ifPresent(this::redirectAttemptParam);
            config.get("max-redirects").asInt().ifPresent(this::maxRedirects);
            config.get("force-https-redirects").asBoolean().ifPresent(this::forceHttpsRedirects);

            config.get("cors").map(CrossOriginConfig::create).ifPresent(this::crossOriginConfig);

            config.get("token-refresh-before-expiration").as(Duration.class).ifPresent(this::tokenRefreshSkew);

            config.get("token-signature-validation").asBoolean().ifPresent(this::tokenSignatureValidation);
            config.get("id-token-signature-validation").asBoolean().ifPresent(this::idTokenSignatureValidation);
            config.get("access-token-ip-check").asBoolean().ifPresent(this::accessTokenIpCheck);

            config.get("tenants").asList(Config.class)
                    .ifPresent(confList -> confList.forEach(tenantConfig -> tenantFromConfig(config, tenantConfig)));

            return this;
        }

        private void tenantFromConfig(Config defaultConfig, Config tenantConfig) {
            addTenantConfig(TenantConfig.tenantBuilder().config(defaultConfig).config(tenantConfig).build());
        }

        /**
         * Amount of time access token should be refreshed before its expiration time.
         * Default is 5 seconds.
         *
         * @param tokenRefreshSkew time to refresh token before expiration
         * @return updated builder
         */
        public Builder tokenRefreshSkew(Duration tokenRefreshSkew) {
            this.tokenRefreshSkew = tokenRefreshSkew;
            return this;
        }

        /**
         * Assign cross-origin resource sharing settings.
         *
         * @param crossOriginConfig cross-origin settings to apply to the redirect endpoint
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cors")
        public Builder crossOriginConfig(CrossOriginConfig crossOriginConfig) {
            this.crossOriginConfig = crossOriginConfig;
            return this;
        }

        /**
         * Whether to enable logout support.
         * When logout is enabled, we use two cookies (User token and user ID token) and we expose
         * an endpoint {@link #logoutUri(String)} that can be used to log the user out from Helidon session
         * and also from OIDC session (uses {@link #logoutEndpointUri(java.net.URI)} on OIDC server).
         * Logout support is disabled by default.
         *
         * @param logoutEnabled whether to enable logout
         * @return updated builder instance
         */
        public Builder logoutEnabled(Boolean logoutEnabled) {
            this.logoutEnabled = logoutEnabled;
            return this;
        }

        /**
         * By default, the client should redirect to the identity server for the user to log in.
         * This behavior can be overridden by setting redirect to false. When token is not present in the request, the client
         * will not redirect and just return appropriate error response code.
         *
         * @param redirect Whether to redirect to OIDC server in case the request does not contain sufficient information to
         *                 authenticate the user, defaults to true
         * @return updated builder instance
         */
        @ConfiguredOption("false")
        public Builder redirect(boolean redirect) {
            this.redirect = redirect;
            return this;
        }

        /**
         * Full URI of this application that is visible from user browser.
         * Used to redirect request back from identity server after successful login.
         *
         * @param uri the frontend URI, such as "http://my.server.com/myApp
         * @return updated builder instance
         */
        @ConfiguredOption
        public Builder frontendUri(String uri) {
            this.frontendUri = uri;
            return this;
        }

        /**
         * Force HTTPS for redirects to identity provider.
         * Defaults to {@code false}.
         *
         * @param forceHttpsRedirects flag to redirect with https
         * @return updated builder instance
         */
        @ConfiguredOption("false")
        public Builder forceHttpsRedirects(boolean forceHttpsRedirects) {
            this.forceHttpsRedirects = forceHttpsRedirects;
            return this;
        }

        /**
         * Can be set to {@code true} to force the use of relative URIs in all requests,
         * regardless of the presence or absence of proxies or no-proxy lists. By default,
         * requests that use the Proxy will have absolute URIs. Set this flag to {@code true}
         * if the host is unable to accept absolute URIs.
         * Defaults to {@value #DEFAULT_RELATIVE_URIS}.
         *
         * @param relativeUris relative URIs flag
         * @return updated builder instance
         */
        @ConfiguredOption("false")
        public Builder relativeUris(boolean relativeUris) {
            this.relativeUris = relativeUris;
            return this;
        }

        /**
         * URI to register web server component on, used by the OIDC server to
         * redirect authorization requests to after a user logs in or approves
         * scopes.
         * Note that usually the redirect URI configured here must be the
         * same one as configured on OIDC server.
         *
         * <p>
         * Defaults to {@value #DEFAULT_REDIRECT_URI}
         *
         * @param redirectUri the URI (path without protocol, host and port) used to redirect requests back to us
         * @return updated builder instance
         */
        @ConfiguredOption(value = DEFAULT_REDIRECT_URI)
        public Builder redirectUri(String redirectUri) {
            this.redirectUri = redirectUri;
            return this;
        }

        /**
         * Path to register web server for logout link.
         * This should be used by application to redirect user to logout the current user
         * from Helidon based session (when using cookies and redirection).
         * This endpoint will logout user from Helidon session (remove Helidon cookies) and redirect user to
         * logout screen of the OIDC server.
         *
         * @param logoutUri URI path for logout component
         * @return updated builder instance
         */
        public Builder logoutUri(String logoutUri) {
            this.logoutUri = logoutUri;
            return this;
        }

        /**
         * URI to redirect to once the logout process is done.
         * The endpoint should not be protected by OIDC (as this would serve no purpose, just to log the user in again).
         * This endpoint usually must be registered with the application as the allowed post-logout redirect URI.
         * Note that the URI should not contain any query parameters. You can obtain state using the
         * state query parameter that must be provided to {@link #logoutUri(String)}.
         *
         * @param uri this will be used by the OIDC server to redirect user to once logout is done, can define just path,
         *            in which case the scheme, host and port will be taken from request.
         * @return updated builder instance
         */
        public Builder postLogoutUri(URI uri) {
            this.postLogoutUri = uri;
            return this;
        }

        /**
         * Configure the parameter used to store the number of attempts in redirect.
         * <p>
         * Defaults to {@value #DEFAULT_ATTEMPT_PARAM}
         *
         * @param paramName name of the parameter used in the state parameter
         * @return updated builder instance
         */
        @ConfiguredOption(value = DEFAULT_ATTEMPT_PARAM)
        public Builder redirectAttemptParam(String paramName) {
            this.redirectAttemptParam = paramName;
            return this;
        }

        /**
         * Configure maximal number of redirects when redirecting to an OIDC provider within a single authentication
         * attempt.
         * <p>
         * Defaults to {@value #DEFAULT_MAX_REDIRECTS}
         *
         * @param maxRedirects maximal number of redirects from Helidon to OIDC provider
         * @return updated builder instance
         */
        @ConfiguredOption("5")
        public Builder maxRedirects(int maxRedirects) {
            this.maxRedirects = maxRedirects;
            return this;
        }

        /**
         * Proxy protocol to use when proxy is used.
         * Defaults to {@value DEFAULT_PROXY_PROTOCOL}.
         *
         * @param protocol protocol to use (such as https)
         * @return updated builder instance
         */
        @ConfiguredOption(value = DEFAULT_PROXY_PROTOCOL)
        public Builder proxyProtocol(String protocol) {
            this.proxyProtocol = protocol;
            return this;
        }

        /**
         * Proxy host to use. When defined, triggers usage of proxy for HTTP requests.
         * Setting to empty String has the same meaning as setting to null - disables proxy.
         *
         * @param proxyHost host of the proxy
         * @return updated builder instance
         * @see #proxyProtocol(String)
         * @see #proxyPort(int)
         */
        @ConfiguredOption
        public Builder proxyHost(String proxyHost) {
            if ((proxyHost == null) || proxyHost.isEmpty()) {
                this.proxyHost = null;
            } else {
                this.proxyHost = proxyHost;
            }
            return this;
        }

        /**
         * Proxy port.
         * Defaults to {@value DEFAULT_PROXY_PORT}
         *
         * @param proxyPort port of the proxy server to use
         * @return updated builder instance
         */
        @ConfiguredOption("80")
        public Builder proxyPort(int proxyPort) {
            this.proxyPort = proxyPort;
            return this;
        }

        /**
         * A {@link TokenHandler} to
         * process header containing a JWT.
         * Default is "Authorization" header with a prefix "bearer ".
         *
         * @param tokenHandler token handler to use
         * @return updated builder instance
         */
        @ConfiguredOption(key = "header-token")
        public Builder headerTokenHandler(TokenHandler tokenHandler) {
            this.headerHandler = tokenHandler;
            return this;
        }

        /**
         * Whether to expect JWT in a header field.
         *
         * @param useHeader set to true to use a header extracted with {@link #headerTokenHandler(TokenHandler)}
         * @return updated builder instance
         */
        @ConfiguredOption(key = "header-use", value = "true")
        public Builder useHeader(Boolean useHeader) {
            this.useHeader = useHeader;
            return this;
        }

        /**
         * Name of a query parameter that contains the JWT access token when parameter is used.
         *
         * @param paramName name of the query parameter to expect
         * @return updated builder instance
         */
        @ConfiguredOption(key = "query-param-name", value = DEFAULT_PARAM_NAME)
        public Builder paramName(String paramName) {
            this.paramName = paramName;
            return this;
        }

        /**
         * Name of a query parameter that contains the JWT id token when parameter is used.
         *
         * @param idTokenParamName name of the query parameter to expect
         * @return updated builder instance
         */
        @ConfiguredOption(key = "query-id-token-param-name", value = DEFAULT_ID_TOKEN_PARAM_NAME)
        public Builder idTokenParamName(String idTokenParamName) {
            this.idTokenParamName = idTokenParamName;
            return this;
        }

        /**
         * Name of a query parameter that contains the tenant name when the parameter is used.
         * Defaults to {@link #DEFAULT_TENANT_PARAM_NAME}.
         *
         * @param paramName name of the query parameter to expect
         * @return updated builder instance
         */
        @ConfiguredOption(key = "query-param-tenant-name", value = DEFAULT_TENANT_PARAM_NAME)
        public Builder paramTenantName(String paramName) {
            this.tenantParamName = paramName;
            return this;
        }

        /**
         * Whether to use a query parameter to send JWT token from application to this
         * server.
         *
         * @param useParam whether to use a query parameter (true) or not (false)
         * @return updated builder instance
         * @see #paramName(String)
         */
        @ConfiguredOption(key = "query-param-use", value = "false")
        public Builder useParam(Boolean useParam) {
            this.useParam = useParam;
            return this;
        }

        /**
         * Name of the encryption configuration available through {@link Security#encrypt(String, byte[])} and
         * {@link Security#decrypt(String, String)}.
         * If configured and encryption is enabled for any cookie,
         * Security MUST be configured in global or current {@code io.helidon.common.context.Context} (this
         * is done automatically in Helidon MP).
         *
         * @param cookieEncryptionName name of the encryption configuration in security used to encrypt/decrypt cookies
         * @return updated builder
         */
        @ConfiguredOption
        public Builder cookieEncryptionName(String cookieEncryptionName) {
            this.tokenCookieBuilder.encryptionName(cookieEncryptionName);
            this.idTokenCookieBuilder.encryptionName(cookieEncryptionName);
            this.tenantCookieBuilder.encryptionName(cookieEncryptionName);
            this.refreshTokenCookieBuilder.encryptionName(cookieEncryptionName);
            this.stateCookieBuilder.encryptionName(cookieEncryptionName);
            return this;
        }

        /**
         * Master password for encryption/decryption of cookies. This must be configured to the same value on each microservice
         * using the cookie.
         *
         * @param cookieEncryptionPassword encryption password
         * @return updated builder
         */
        @ConfiguredOption
        public Builder cookieEncryptionPassword(char[] cookieEncryptionPassword) {
            this.tokenCookieBuilder.encryptionPassword(cookieEncryptionPassword);
            this.idTokenCookieBuilder.encryptionPassword(cookieEncryptionPassword);
            this.tenantCookieBuilder.encryptionPassword(cookieEncryptionPassword);
            this.refreshTokenCookieBuilder.encryptionPassword(cookieEncryptionPassword);
            this.stateCookieBuilder.encryptionPassword(cookieEncryptionPassword);
            return this;
        }

        /**
         * Whether to encrypt token cookie created by this microservice.
         * Defaults to {@code false}.
         *
         * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from
         *                                OIDC server {@code false}
         * @return updated builder instance
         */
        @ConfiguredOption(value = "false")
        public Builder cookieEncryptionEnabled(boolean cookieEncryptionEnabled) {
            this.tokenCookieBuilder.encryptionEnabled(cookieEncryptionEnabled);
            return this;
        }

        /**
         * Whether to encrypt id token cookie created by this microservice.
         * Defaults to {@code true}.
         *
         * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from
         *                                OIDC server {@code false}
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookie-encryption-id-enabled", value = "true")
        public Builder cookieEncryptionEnabledIdToken(boolean cookieEncryptionEnabled) {
            this.idTokenCookieBuilder.encryptionEnabled(cookieEncryptionEnabled);
            return this;
        }

        /**
         * Whether to encrypt tenant name cookie created by this microservice.
         * Defaults to {@code true}.
         *
         * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as plain text name {@code false}
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookie-encryption-tenant-enabled", value = "true")
        public Builder cookieEncryptionEnabledTenantName(boolean cookieEncryptionEnabled) {
            this.tenantCookieBuilder.encryptionEnabled(cookieEncryptionEnabled);
            return this;
        }

        /**
         * Whether to encrypt refresh token cookie created by this microservice.
         * Defaults to {@code true}.
         *
         * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from
         *                                OIDC server {@code false}
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookie-encryption-refresh-enabled", value = "true")
        public Builder cookieEncryptionEnabledRefreshToken(boolean cookieEncryptionEnabled) {
            this.refreshTokenCookieBuilder.encryptionEnabled(cookieEncryptionEnabled);
            return this;
        }

        /**
         * Whether to encrypt state cookie created by this microservice.
         * Defaults to {@code true}.
         *
         * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as sent to
         *                                OIDC server {@code false}
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookie-encryption-state-enabled", value = "true")
        public Builder cookieEncryptionEnabledState(boolean cookieEncryptionEnabled) {
            this.stateCookieBuilder.encryptionEnabled(cookieEncryptionEnabled);
            return this;
        }

        /**
         * When using cookie, used to set the SameSite cookie value. Can be
         * "Strict" or "Lax"
         *
         * @param sameSite SameSite cookie attribute value
         * @return updated builder instance
         */
        public Builder cookieSameSite(String sameSite) {
            return cookieSameSite(SetCookie.SameSite.valueOf(sameSite.toUpperCase(Locale.ROOT)));
        }

        /**
         * When using cookie, used to set the SameSite cookie value. Can be
         * "Strict" or "Lax".
         *
         * @param sameSite SameSite cookie attribute
         * @return updated builder instance
         */
        @ConfiguredOption(value = "LAX")
        public Builder cookieSameSite(SetCookie.SameSite sameSite) {
            this.tokenCookieBuilder.sameSite(sameSite);
            this.idTokenCookieBuilder.sameSite(sameSite);
            this.tenantCookieBuilder.sameSite(sameSite);
            this.refreshTokenCookieBuilder.sameSite(sameSite);
            this.stateCookieBuilder.sameSite(sameSite);
            this.cookieSameSiteDefault = false;
            return this;
        }

        /**
         * When using cookie, if set to true, the Secure attribute will be configured.
         * Defaults to false.
         *
         * @param secure whether the cookie should be secure (true) or not (false)
         * @return updated builder instance
         */
        @ConfiguredOption("false")
        public Builder cookieSecure(Boolean secure) {
            this.tokenCookieBuilder.secure(secure);
            this.idTokenCookieBuilder.secure(secure);
            this.tenantCookieBuilder.secure(secure);
            this.refreshTokenCookieBuilder.secure(secure);
            this.stateCookieBuilder.secure(secure);
            return this;
        }

        /**
         * When using cookie, if set to true, the HttpOnly attribute will be configured.
         * Defaults to {@value OidcCookieHandler.Builder#DEFAULT_HTTP_ONLY}.
         *
         * @param httpOnly whether the cookie should be HttpOnly (true) or not (false)
         * @return updated builder instance
         */
        @ConfiguredOption("true")
        public Builder cookieHttpOnly(Boolean httpOnly) {
            this.tokenCookieBuilder.httpOnly(httpOnly);
            this.idTokenCookieBuilder.httpOnly(httpOnly);
            this.tenantCookieBuilder.httpOnly(httpOnly);
            this.refreshTokenCookieBuilder.httpOnly(httpOnly);
            this.stateCookieBuilder.httpOnly(httpOnly);
            return this;
        }

        /**
         * When using cookie, used to set MaxAge attribute of the cookie, defining how long
         * the cookie is valid.
         * Not used by default.
         *
         * @param age age in seconds
         * @return updated builder instance
         */
        @ConfiguredOption
        public Builder cookieMaxAgeSeconds(long age) {
            this.tokenCookieBuilder.maxAge(age);
            this.idTokenCookieBuilder.maxAge(age);
            this.tenantCookieBuilder.maxAge(age);
            this.refreshTokenCookieBuilder.maxAge(age);
            this.stateCookieBuilder.maxAge(age);
            return this;
        }

        /**
         * Path the cookie is valid for.
         * Defaults to "/".
         *
         * @param path the path to use as value of cookie "Path" attribute
         * @return updated builder instance
         */
        @ConfiguredOption(value = OidcCookieHandler.Builder.DEFAULT_PATH)
        public Builder cookiePath(String path) {
            this.tokenCookieBuilder.path(path);
            this.idTokenCookieBuilder.path(path);
            this.tenantCookieBuilder.path(path);
            this.refreshTokenCookieBuilder.path(path);
            this.stateCookieBuilder.path(path);
            return this;
        }

        /**
         * Domain the cookie is valid for.
         * Not used by default.
         *
         * @param domain domain to use as value of cookie "Domain" attribute
         * @return updated builder instance
         */
        @ConfiguredOption
        public Builder cookieDomain(String domain) {
            this.tokenCookieBuilder.domain(domain);
            this.idTokenCookieBuilder.domain(domain);
            this.tenantCookieBuilder.domain(domain);
            this.refreshTokenCookieBuilder.domain(domain);
            this.stateCookieBuilder.domain(domain);
            return this;
        }

        /**
         * Name of the cookie to use.
         * Defaults to {@value #DEFAULT_COOKIE_NAME}.
         *
         * @param cookieName name of a cookie
         * @return updated builder instance
         */
        @ConfiguredOption(value = DEFAULT_COOKIE_NAME)
        public Builder cookieName(String cookieName) {
            this.tokenCookieBuilder.cookieName(cookieName);
            return this;
        }

        /**
         * Name of the cookie to use for id token.
         * Defaults to {@value #DEFAULT_COOKIE_NAME}_2.
         *
         * This cookie is only used when logout is enabled, as otherwise it is not needed.
         * Content of this cookie is encrypted.
         *
         * @param cookieName name of a cookie
         * @return updated builder instance
         */
        @ConfiguredOption(DEFAULT_ID_COOKIE_NAME)
        public Builder cookieNameIdToken(String cookieName) {
            this.idTokenCookieBuilder.cookieName(cookieName);
            return this;
        }

        /**
         * The name of the cookie to use for the tenant name.
         * Defaults to {@value #DEFAULT_TENANT_COOKIE_NAME}.
         *
         * @param cookieName name of a cookie
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookie-name-tenant", value = DEFAULT_TENANT_COOKIE_NAME)
        public Builder cookieTenantName(String cookieName) {
            this.tenantCookieBuilder.cookieName(cookieName);
            return this;
        }

        /**
         * The name of the cookie to use for the refresh token.
         * Defaults to {@value #DEFAULT_REFRESH_COOKIE_NAME}.
         *
         * @param cookieName name of a cookie
         * @return updated builder instance
         */
        @ConfiguredOption(DEFAULT_REFRESH_COOKIE_NAME)
        public Builder cookieNameRefreshToken(String cookieName) {
            this.refreshTokenCookieBuilder.cookieName(cookieName);
            return this;
        }

        /**
         * The name of the cookie to use for the state storage.
         * Defaults to {@value #DEFAULT_STATE_COOKIE_NAME}.
         *
         * @param cookieName name of a cookie
         * @return updated builder instance
         */
        @ConfiguredOption(DEFAULT_REFRESH_COOKIE_NAME)
        public Builder cookieNameState(String cookieName) {
            this.stateCookieBuilder.cookieName(cookieName);
            return this;
        }

        /**
         * Whether to use cookie to store JWT between requests.
         * Defaults to {@value #DEFAULT_COOKIE_USE}.
         *
         * @param useCookie whether to use cookie to store JWT (true) or not (false))
         * @return updated builder instance
         */
        @ConfiguredOption(key = "cookie-use", value = "true")
        public Builder useCookie(Boolean useCookie) {
            this.useCookie = useCookie;
            return this;
        }

        /**
         * Add specific {@link TenantConfig} instance.
         *
         * @param tenantConfig tenant configuration
         * @return updated builder instance
         */
        @ConfiguredOption(key = "tenants", type = TenantConfig.class, description = "Configurations of the tenants")
        public Builder addTenantConfig(TenantConfig tenantConfig) {
            tenantConfigurations.put(tenantConfig.name(), tenantConfig);
            return this;
        }

        /**
         * Whether access token signature check should be enabled.
         * Signature check is enabled by default, and it is highly recommended to not change that.
         * Change this setting only when you really know what you are doing, otherwise it could case security issues.
         *
         * @param enabled whether access token signature check is enabled
         * @return updated builder instance
         */
        @ConfiguredOption("true")
        public Builder tokenSignatureValidation(boolean enabled) {
            tokenSignatureValidation = enabled;
            return this;
        }

        /**
         * Whether id token signature check should be enabled.
         * Signature check is enabled by default, and it is highly recommended to not change that.
         * Change this setting only when you really know what you are doing, otherwise it could case security issues.
         *
         * @param enabled whether id token signature check is enabled
         * @return updated builder instance
         */
        @ConfiguredOption("true")
        public Builder idTokenSignatureValidation(boolean enabled) {
            idTokenSignatureValidation = enabled;
            return this;
        }

        /**
         * Whether to check if current IP address matches the one access token was issued for.
         * This check helps with cookie replay attack prevention.
         *
         * @param enabled whether to check if current IP address matches the one access token was issued for
         * @return updated builder instance
         */
        @ConfiguredOption("true")
        public Builder accessTokenIpCheck(boolean enabled) {
            accessTokenIpCheck = enabled;
            return this;
        }

    }
}
